《Effective Java》读书笔记

构造函数 VS 静态工厂方法

静态工厂方法与设计模式无关,只是一种创建实例的方式,如:

public static Boolean valueOf(boolean b) {
return (b ? TRUE : FALSE);
}

与传统的构造函数相比,使用静态工厂方法的优势有三点

  1. 静态工厂方法的命名更灵活,使用者不需要查看API也能通过静态工厂方法创建想要的实例
  2. 静态工厂方法创建的对象是单例的
    Boolean fa = Boolean.valueOf(false);
    Boolean fb = Boolean.valueOf(false);
    assertTrue(fa == fb);

    Boolean ca = new Boolean(false);
    Boolean cb = new Boolean(false);
    assertTrue(ca != cb);
    assertTur(fa.equals(ca));

如果比较的两个对象都是单例的,那么通过 ==而非equals方法来判断俩对象是否相同的效率更高

  1. 静态工厂方法能返回员返回类型的任意子类型的对象:

    EnumSet是一个抽象类,RegularEnumSet以及JumboEnumSet是其实现类,使用者可以通过以下静态工厂方法创建一个存放枚举的集合,而这个集合的实现类是什么,使用者并不关心。使用者关心的是EnumSet提供的API方法能否正常使用就可以了。

    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
    Enum<?>[] universe = getUniverse(elementType);
    if (universe == null)
    throw new ClassCastException(elementType + " not an enum");

    if (universe.length <= 64)
    return new RegularEnumSet<>(elementType, universe);
    else
    return new JumboEnumSet<>(elementType, universe);
    }

仅有的劣势:如果某个类只能通过静态工厂方法实例化,而缺乏public或者protected的构造方法因而无法扩展。

如何构造多属性的对象

当我们需要构建一个包含最多6个,最少1个属性值的对象时,常用的方式有以下几种

1. 重构构造函数

提供多个包含不同属性值的构造函数

这种方式有很明显的缺陷:

  • 随着属性值数量的增大构造函数将变得非常庞大、臃肿,可读性较差
  • 在给构造函数传递参数时很容易搞错(虽然现在强大如IDEA这样的编辑器会有提示参数名的功能)。比如一个Human类中身高以及体重参数的类型都是Double,传递参数时如果将身高体重搞混了编译器并不会报错,但是这样的结果难免就是很奇葩了。

2. 使用JavaBeans模式

这种模式就是先通过无参构造函数创建一个空的对象,然后使用setter方法对属性进行赋值,这种方式虽然提高了可读性,但是存在较为严重的缺陷:无法保证状态一致性,并且无法创建不可变的对象,安全性较差

3. builder模式【推荐】

builder模式不直接生成对象,而是通过调用构造器传入必要的参数获得一个内部builder对象,通过调用该对象的build方法获得一个不可变的对象。

  • 使用builder模式创建对象提高了可读性以及可扩展性,
  • 一般只有在参数可数大于4的情况下才会使用builder模式,尤其适用于参数可选的情况下,springKakfa在生成Container时我们就采用的builder模式。

类与接口

封装是软件设计的基本原则之一,设计良好的模块会隐藏所有的实现细节,将API与实现隔离开来

尽可能地使每个类或者成员不被外界访问

  • 如果在一个发行的版本中某个类或者接口是共有的,那么你就有责任永远支持它已保证其兼容性!

  • 成员的4种访问级别:

    • public:在任何地方都可以访问该成员
    • protected:声明该成员的类的子类可以访问这个成员
    • package private:声明该成员的包内部的类可以访问这个成员【default】
    • private:声明该成员的顶层类内部可以访问
  • 如果子类覆盖了父类的某个方法,那么子类中对应方法的访问权限不能低于父类的。(如果覆盖了父类的protected方法,那么子类中该方法必须声明为protected或者public)

  • 因为接口中所有方法都隐含着public访问级别,因此实现接口的方法必须声明为public。

通过对象引用实现策略模式

在C语言中可以通过函数指针来实现策略模式,比如qsort函数要求用一个指向comparator函数的指针作为参数。在Java中没有函数指针的概念,但是可以通过对象引用来实现同样的功能。

下面的代码展示了最简单的策略使用:
首先定义一个具体的策略类,考虑到策略类会被频繁使用,并且该类是无状态的,所以使用单例模式来导出策略类的实例比较好,这样能减少不必要的对象创建开销

public class StringLengthComparator {
private StringLengthComparator() {}

public static final StringLengthComparator SLC = new StringLengthComparator();

public int compare(String s1, String s2) {
return s1.length() - s2.length();
}
}

然后在一个客户端方法中将策略类当做参数传入:

public class StringArrayUtil {

public void sortStringList(List<String> list, StringLengthComparator comparator) {
Assert.assertTrue(list.size() > 2);
if (comparator.compare(list.get(0), list.get(1)) > 0) {
String tmp = list.get(0);
list.set(0, list.get(1));
list.set(1, tmp);
}
}
}

这里传入的参数限定为具体策略类不方便扩展,所以可以定义一个接口:

public interface Comparator<String> {
public int compare(String s1, String s2);
}

客户端的声明可以改为:public void sortStringList(List<String> list, Comparator comparator)

具体的策略类往往使用匿名类声明,比如最常见的:

Arrays.sort(array, new Comparator<String>() {
public int compare(String s1, String s2) {
return s1.length() -s2.length();
}
});

以这种方式使用匿名类时,每次执行调用的时候都会创建一个新的实例,可以考虑将函数对象存储到一个私有的静态final域里。

泛型

泛型相关的术语

参数化类型是不可变的

对于不同的类型Type1和Type2,List<Type1>既不是List<Type2>的子类型也不是其超类型。
我们可以将任意对象放到List<Object>中,但是却只能将字符串对象放到List<String>

Stack为例:

public class Stack<E> extends Vector<E> {
...
public E push(E item) {
addElement(item);
return item;
}
}

由于子类型的对象可以转化成父类型,所以以下操作可行的:

Stack<Number> stack = new Stack<>();
stack.push(new Integer(1));

但是如果我在自己的Stack中要实现一个pushAll的功能,将一组数据push到栈中:

public class MyStack<E> extends Stack<E>{
public void pushAll(Iterable<E> src) {
for (E e : src) {
push(e);
}
}
}

当传入数据的类型与Stack的类型不一致时,即使传入对象是Stack类型的子类型,编译期会提示错误

有限制通配符的妙用

前面的pushAll方法其参数称为:E的Iterable接口 ,通过有限制通配符将参数改为:E的某个子类型的Iterable接口public void pushAll(Iterable<? extends E> src)

接下来,需要实现一个popAll的方法将Stack中的数据pop到一个集合中,首先想到的实现是这样的:

public void popAll(Collection<E> dest) {
while (!this.isEmpty()) {
dest.add(pop());
}
}

这样的实现有个限制,就是只能导出到一个类似于List<Number>的集合中,如果尝试导出到List<Object>则会报错:

同样的借助于有限制通配符将popAll的参数由E的集合类型转化成E的某种超类的集合
public void popAll(Collection<? super E> dest)

类型安全的异构容器

泛型常用于容器,参数化的容器一般限制了参数类型的数量,如一个List只有一个类型参数,一个Map有两个类型参数。

如果想要获得更多的灵活性,就需要对参数进行泛型化,而不是对容器进行泛型化。考虑下面这个类:

public class Favorites {
private static Map<Class<?>, Object> map = new HashMap<>();

public static <T> void put(Class<T> type, T instance) {
map.put(type, instance);
}

public static <T> T get(Class<T> type) {
return (T) map.get(type);
}
}

  • 在Java 1.5之后对Class进行了泛型化处理
  • 对map的Key进行了泛型化,这样的话我们就能将不同类型的对象存到容器中,这就是异构
  • Map的value类型是Object,因此容器并不能保证键值对之间的类型关系,如果传入原生态的Class,那么就可恶意的将一个String对象映射到其他类型,进而破坏Favorites的内部结构
    Class c = Integer.class;
    Favorites.put(c, "da");
    System.out.println(Favorites.get(Integer.class));

为了避免出现不可预料的运行时异常,在put过程应该严格把关,确保传入的类型与对象类型是一致的,这可以借助于Class的cast方法:

public T cast(Object obj) {
if (obj != null && !isInstance(obj))
throw new ClassCastException(cannotCastMsg(obj));
return (T) obj;
}

其中isInstance方法能判断对象是否为指定类的对象。

异常与并发

ConcurrentModificationException

这是Java提供的一种标准异常,适用于的场景为:在禁止并发修改的情况下检测到了对象的并发修改

看下面一个简单的例子:

public void testConcurrentModificationException() {
Set<Integer> set = new HashSet<>(Arrays.asList(1,2,3));
for (Integer i : set) {
System.out.println(i);
if (i == 2) {
set.remove(3);
}
}
}

输出的结果:

1
2

java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1437)
at java.util.HashMap$KeyIterator.next(HashMap.java:1461)
at Test.testConcurrentModificationException(Test.java:549)

只要i<=2就会报这个错,因为企图在遍历列表的过程中,将一个元素从列表中删除是非法的